﻿//Copyright 2011 - Jonathan Allen

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

namespace Granite.Modeling.Internals
{
    /// <summary>
    /// Property bag with two-level change tracking capabilities.
    /// </summary>
    public class EditableObjectPropertyBag : ChangeTrackingPropertyBag
    {
        readonly Dictionary<String, object> m_CheckpointValues = new Dictionary<string, object>(StringComparer.Ordinal);

        /// <summary>
        /// Property bag with two-level change tracking capabilities.
        /// </summary>
        /// <param name="owner"></param>
        public EditableObjectPropertyBag(object owner)
            : base(owner)
        {
            Contract.Requires(owner != null);
        }

        /// <summary>
        /// This creates a checkpoint using the current values. The checkpoint remains available until EndEdit or CancelEdit is called.
        /// </summary>
        /// <remarks>Calling this method multiple times will have no effect.</remarks>
        [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "BeginEdit")]
        public void BeginEdit()
        {
            Contract.Ensures(IsEditing == true);

            if (IsEditing)
                return;

            if (BlockReentrant)
                throw new InvalidOperationException("Reentrant call to BeginEdit detected");

            IsEditing = true;
            BlockReentrant = true;

            foreach (var item in Values)
            {
                m_CheckpointValues.Add(item.Key, item.Value);
                if (item.Value is IEditableObject)
                    ((IEditableObject)item.Value).BeginEdit();
            }

            BlockReentrant = false;
            Contract.Assume(IsEditing == true);
        }

        /// <summary>
        /// This reverts all changes to the checkpoint values. CancelEdit is called recursively on any value in the original list. 
        /// </summary>
        /// <remarks>Calling this when there are no matching BeginEdit will have no effect.</remarks>
        [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "CancelEdit")]
        public void CancelEdit()
        {
            Contract.Ensures(IsEditing == false);

            if (!IsEditing)
                return;

            if (BlockReentrant)
                throw new InvalidOperationException("Reentrant call to CancelEdit detected");

            IsEditing = false;
            BlockReentrant = true;

            //remove properties that no longer exist
            foreach (var item in Values.ToList())
            {
                if (!m_CheckpointValues.ContainsKey(item.Key))
                {
                    Values.Remove(item.Key);
                    var property = Metadata.Properties[item.Key];
                    OnPropertyChanged(property);
                    OnRevalidateProperty(property);
                }
            }

            //update remaining properties
            foreach (var item in m_CheckpointValues)
            {
                var oldValue = GetValue(item.Key);
                if (!object.Equals(oldValue, item.Value))
                {
                    Values[item.Key] = item.Value;
                    var property = Metadata.Properties[item.Key];
                    OnPropertyChanged(property);
                    OnRevalidateProperty(property);
                }
            }

            //recusively call CancelEdit			
            foreach (var item in m_CheckpointValues)
            {
                if (item.Value is IEditableObject)
                    ((IEditableObject)item.Value).CancelEdit();
            }

            OnRevalidateObject();
            m_CheckpointValues.Clear();


            BlockReentrant = false;
            Contract.Assume(IsEditing == false);
        }

        /// <summary>
        /// This removed one level of checkpoint values. 
        /// EndEdit is called recursively on any value in the list of checkpoint values. 
        /// </summary>
        /// <remarks>Calling this when there are no matching BeginEdit will have no effect.</remarks>
        [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "EndEdit")]
        public void EndEdit()
        {
            Contract.Ensures(IsEditing == false);

            if (!IsEditing)
                return;

            if (BlockReentrant)
                throw new InvalidOperationException("Reentrant call to EndEdit detected");
            
            IsEditing = false;
            BlockReentrant = true;

            foreach (var item in m_CheckpointValues)
            {
                if (item.Value is IEditableObject)
                    ((IEditableObject)item.Value).EndEdit();
            }
            m_CheckpointValues.Clear();

            BlockReentrant = false;
            Contract.Assume(IsEditing == false);
        }

        /// <summary>
        /// Marks all fields as unchanged by storing them in the original values collection.
        /// </summary>
        /// <remarks>Calling this ends all pending edits.</remarks>
        public override void AcceptChanges(bool recursive)
        {
            Contract.Ensures(IsEditing == false);

            EndEdit();
            base.AcceptChanges(recursive);

            Contract.Assume(IsEditing == false);
        }

        /// <summary>
        /// Replaces the current values collection with the original values collection.
        /// </summary>
        /// <remarks>Calling this cancels all pending edits.</remarks>
        public override void RejectChanges(bool recursive)
        {
            Contract.Ensures(IsEditing == false);

            CancelEdit();
            base.RejectChanges(recursive);

            Contract.Assume(IsEditing == false);
        }

        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [ContractInvariantMethod]
        void ObjectInvariant()
        {
            Contract.Invariant(m_CheckpointValues != null);
        }

        /// <summary>
        /// Currently editing
        /// </summary>
        public bool IsEditing { get; private set; }


        /// <summary>
        /// Used to prevent reentrant calls to Begin/End/Cancel Edit
        /// </summary>
        bool BlockReentrant{ get; set; }

    }
}
